Работа с git
Аскеров А.Э.
Российский университет дружбы народов, Москва, Россия
22 февраля 2025
Приобрести практические навыки работы с системой управления версиями Git.
Изучить работу Git.
В случае если ранее git не использовался, для начала нам необходимо осуществить установку. Выполним следующие команды, чтобы git узнал наше имя и электронную почту.
Укажем параметры установки окончаний строк со значениями true и input.
Установим соответствующий флаг, чтобы избежать нечитаемых строк.
Начнём работу в пустом рабочем каталоге с создания пустого каталога с именем hello, затем войдём в него и создадим там файл с именем hello.html.
Чтобы создать git репозиторий из этого каталога, выполним команду git init.
Добавим файл в репозиторий.
Проверим текущее состояние репозитория.
Видим, что коммитить нечего – в репозитории хранится текущее состояние рабочего каталога, и нет никаких изменений, ожидающих записи.
Добавим кое-какие HTML-теги к нашему приветствию. Изменим содержимое файла hello.html на следующее.
Проверим состояние рабочего каталога.
Видно, что git знает, что файл hello.html был изменён, но при этом эти изменения ещё не зафиксированы в репозитории.
Теперь выполним команду git, чтобы проиндексировать изменения. Проверим состояние.
Изменения файла hello.html были проиндексированы. Это означает, что git теперь знает об изменении, но изменение пока не записано в репозиторий. Следующий коммит будет включать в себя проиндексированные изменения.
Сделаем коммит и проверим состояние.
Откроется редактор.
В первой строке введём комментарий: «Added h1 tag». Сохраним файл и выйдем из редактора.
Теперь ещё раз проверим состояние.
Рабочий каталог чистый, можно продолжить работу.
Изменим страницу «Hello World», чтобы она содержала стандартные теги html и body.
Теперь добавим это изменение в индекс git.
Теперь добавим заголовки HTML (секцию head) к странице «Hello World».
Проверим текущий статус.
Обратим внимание на то, что hello.html указан дважды в состоянии. Первое изменение (добавление стандартных тегов) проиндексировано и готово к коммиту. Второе изменение (добавление заголовков HTML) является непроиндексированным. Если бы мы делали коммит сейчас, заголовки не были бы сохранены в репозиторий.
Произведём коммит проиндексированного изменения (значение по умолчанию), а затем ещё раз проверим состояние.
Состояние команды говорит о том, что hello.html имеет незафиксированные изменения, но уже не в буферной зоне.
Теперь добавим второе изменение в индекс, а затем проверим состояние с помощью команды git status.
Второе изменение проиндексировано и готово к коммиту.
Сделаем коммит второго изменения.
Получим список произведённых изменений.
Команда checkout позволяет вернуться назад в истории – она копирует любой снимок из репозитория в рабочий каталог.
Получим хэши предыдущих версий.
Изучим данные лога и найдём хэш для первого коммита. Он должен быть в последней строке данных. Используем этот хэш-код (достаточно первых 7 знаков) в команде ниже. Затем проверим содержимое файла hello.html.
Вернёмся к последней версии в ветке master.
master – имя ветки по умолчанию. Переключая имена веток, мы попадаем на последнюю версию выбранной ветки.
Назовём текущую версию страницы hello первой (v1).
Создадим тег первой версии.
Теперь текущая версия страницы называется v1.
После этого создадим тег для версии, которая идёт перед текущей версией и назовем её v1-beta. В первую очередь нам надо переключиться на предыдущую версию.
Это версия c тегами html и body, но ещё пока без head. Давайте сделаем её версией v1-beta.
Теперь попробуем попереключаться между двумя отмеченными версиями.
Мы можем увидеть, какие теги доступны, используя команду git tag.
Мы также можем посмотреть теги в логе.
Убедимся, что мы находимся на последнем коммите ветки master, прежде чем продолжить работу.
Иногда случается, что мы изменили файл в рабочем каталоге, и хотим отменить последние коммиты. С этим справится команда git checkout.
Внесём изменение в файл hello.html в виде нежелательного комментария.
Сначала проверим состояние рабочего каталога.
Мы видим, что файл hello.html был изменен, но еще не проиндексирован.
Используем команду git checkout для переключения версии файла hello.html в репозитории.
Команда git status показывает нам, что не было произведено никаких изменений, не зафиксированных в рабочем каталоге.
Внесём изменение в файл hello.html в виде нежелательного комментария.
Проиндексируем это изменение.
Проверим состояние нежелательного изменения.
Состояние показывает, что изменение было проиндексировано и готово к коммиту.
К счастью, вывод состояния показывает нам именно то, что мы должны сделать для отмены индексации изменения.
Команда git reset сбрасывает буферную зону к HEAD. Это очищает буферную зону от изменений, которые мы только что проиндексировали.
Наш рабочий каталог опять чист.
Иногда мы понимаем, что новые коммиты являются неверными, и хотим их отменить. Есть несколько способов решения этого вопроса, здесь мы будем использовать самый безопасный. Мы отменим коммит путём создания нового коммита, отменяющего нежелательные изменения.
Изменим файл hello.html на следующий.
Выполним следующие команды.
Чтобы отменить коммит, нам необходимо сделать коммит, который удаляет изменения, сохранённые нежелательным коммитом.
Перейдём в редактор, где мы можем отредактировать коммит-сообщение по умолчанию или оставить все как есть. Сохраним и закроем файл.
Проверка лога показывает нежелательные и отменённые коммиты в наш репозиторий.
git revert является мощной командой, которая позволяет отменить любые коммиты в репозиторий. Однако, и оригинальный и «отменённый» коммиты видны в истории ветки (при использовании команды git log).
Часто мы делаем коммит, и сразу понимаем, что это была ошибка. Было бы неплохо иметь команду «возврата», которая позволила бы нам сделать вид, что неправильного коммита никогда и не было. Команда «возврата» даже предотвратила бы появление нежелательного коммита в истории git log.
При получении ссылки на коммит (т.е. хэш, ветка или имя тега), команда git reset:
Давайте сделаем быструю проверку нашей истории коммитов. Выполним следующее.
Мы видим, что два последних коммита в этой ветке – «Oops» и «Revert Oops». Удалим их с помощью сброса.
Но прежде чем удалить коммиты, отметим последний коммит тегом, чтобы потом можно было его найти.
Глядя на историю лога, мы видим, что коммит с тегом «v1» является коммитом, предшествующим ошибочному коммиту. Сбросим ветку до этой точки. Поскольку ветка имеет тег, мы можем использовать имя тега в команде сброса (если она не имеет тега, мы можем использовать хэш-значение).
Наша ветка master теперь указывает на коммит v1, а коммитов Oops и Revert Oops в ветке уже нет. Параметр –hard указывает, что рабочий каталог должен быть обновлен в соответствии с новым head ветки.
Что же случается с ошибочными коммитами? Оказывается, что коммиты всё ещё находятся в репозитории. На самом деле, мы всё ещё можем на них ссылаться. Например, в начале этого урока мы создали для отменённого коммита тег «oops». Посмотрим на все коммиты.
Мы видим, что ошибочные коммиты не исчезли. Они всё ещё находятся в репозитории. Просто они отсутствуют в ветке master. Если бы мы не отметили их тегами, они по-прежнему находились бы в репозитории, но не было бы никакой возможности ссылаться на них, кроме как при помощи их хэш-имён. Коммиты, на которые нет ссылок, остаются в репозитории до тех пор, пока не будет запущен сборщик мусора.
Сброс в локальных ветках, как правило, безопасен. Последствия любой «аварии» как правило, можно восстановить простым сбросом с помощью нужного коммита. Однако, если ветка «расшарена» на удалённых репозиториях, сброс может сбить с толку других пользователей ветки.
Тег oops свою функцию выполнил. Давайте удалим его и коммиты, на которые он ссылался, сборщиком мусора.
Тег «oops» больше не будет отображаться в репозитории.
Добавим в страницу комментарий автора.
Выполним следующее.
После совершения коммита к комментарию стоит добавить электронную почту автора.
Обновим страницу hello, включив в неё email.
Мы не хотим создавать отдельный коммит только ради электронной почты. Поэтому изменим предыдущий коммит, включив в него адрес электронной почты.
Выполним следующее.
Мы можем увидеть, что оригинальный коммит «автор» заменён коммитом «автор/email». Этого же эффекта можно достичь путём сброса последнего коммита в ветке, и повторного коммита новых изменений.
Сейчас мы собираемся создать структуру нашего репозитория. Перенесём страницу в каталог lib.
Мы могли бы выполнить:
Сделаем коммит этого перемещения.
Добавим файл index.html в наш репозиторий.
Укажем в нём следующее содержимое.
Добавим файл и сделаем коммит.
Теперь при открытии index.html, мы увидим кусок страницы hello в маленьком окошке.
Выполним следующее.
Это каталог, в котором хранится вся информация git.
Выполним следующее.
Мы видим набор каталогов, имена которых состоят из 2 символов. Имена каталогов являются первыми двумя буквами хэша sha1 объекта, хранящегося в git.
Выполним следующее.
Здесь мы смотрим в один из каталогов с именем из 2 букв. В результате мы видим файлы с именами из 38 символов. Это файлы, содержащие объекты, хранящиеся в git. Они сжаты и закодированы, поэтому просмотр их содержимого может мало чем помочь.
Выполним следующее.
Это файл конфигурации, создающийся для каждого конкретного проекта. Записи в этом файле будут перезаписывать записи в файле .gitconfig нашего главного каталога, по крайней мере в рамках этого проекта.
Выполним следующее.
Здесь каждый файл соответствует тегу, ранее созданному с помощью команды git tag. Его содержание – это всего лишь хэш коммита, привязанный к тегу.
Каталог heads практически аналогичен, но используется для веток, а не тегов. На данный момент у нас есть только одна ветка, так что всё, что мы увидим в этом каталоге – это ветка master.
Выполним следующее.
Файл HEAD содержит ссылку на текущую ветку, в данный момент это должна быть ветка master.
Выполним следующее.
Эта команда показывает последний коммит в репозиторий. SHA1 хэш у разных пользователей, вероятно, отличается от этого, но отображаемый результат похож на этот.
Выполним следующее.
Мы можем вывести дерево каталогов, ссылка на который идёт в коммите. Это должно быть описание файлов (верхнего уровня) в нашем проекте (для конкретного коммита). Используем SHA1 хэш из строки «дерева», из списка выше.
Выполним следующее.
Выполним следующее.
Выполним следующее.
Пора сделать наш hello world более выразительным. Так как это может занять некоторое время, лучше переместить эти изменения в отдельную ветку, чтобы изолировать их от изменений в ветке master.
Назовём нашу новую ветку «style».
Выполним следующее.
Команда git status сообщает о том, что мы находимся в ветке «style».
Выполним следующее.
Укажем в нём следующее содержимое.
Выполним следующее.
Обновим файл hello.html, чтобы использовать стили style.css.
Выполним следующее.
Обновим файл index.html, чтобы он тоже использовал style.css.
Выполним следующее.
Теперь в нашем проекте есть две ветки.
Выполним следующее.
Используем команду git checkout для переключения между ветками.
Сейчас мы находимся на ветке master. Это заметно по тому, что файл hello.html не использует стили style.css.
Выполним следующее.
Содержимое lib/hello.html подтверждает, что мы вернулись на ветку style.
Пока мы меняли ветку style, кто-то решил обновить ветку master. Они добавили файл README.md.
Выполним следующее.
Создадим файл README.md.
Выполним следующее.
Теперь у нас в репозитории есть две отличающиеся ветки. Используем следующую лог-команду для просмотра веток и их отличий.
Слияние переносит изменения из двух веток в одну. Вернёмся к ветке style и сольём master со style.
Вернёмся в ветку master.
Внесём следующие изменения.
Выполним следующее.
Выполним следующее.
После коммита «Added README» ветка master была объединена с веткой style, но в настоящее время в master есть дополнительный коммит, который не был слит со style.
Последнее изменение в master конфликтует с некоторыми изменениями в style. На следующем шаге мы решим этот конфликт.
Теперь вернёмся к ветке style и попытаемся объединить её с новой веткой master.
Выполним следующее.
Если мы откроем lib/hello.html, то увидим следующее.
Первый раздел – версия текущей ветки (style). Второй раздел – версия ветки master.
Нам необходимо вручную разрешить конфликт. Внесём изменения в lib/hello.html для достижения следующего результата.
Выполним следующее.
Рассмотрим различия между слиянием и перебазированием. Для того, чтобы это сделать, нам нужно вернуться в репозиторий в момент до первого слияния, а затем повторить те же действия, но с использованием перебазирования вместо слияния.
Мы будем использовать команду reset для возврата веток к предыдущему состоянию.
Вернёмся на ветке style к точке перед тем, как мы слили её с веткой master. Мы можем сбросить ветку к любому коммиту. По сути, это изменение указателя ветки на любую точку дерева коммитов.
В этом случае мы хотим вернуться в ветке style в точку перед слиянием с master. Нам необходимо найти последний коммит перед слиянием.
Выполним следующее.
Мы видим, что коммит «Updated index.html» был последним на ветке style перед слиянием. Сбросим ветку style к этому коммиту.
Поищем лог ветки style. У нас в истории больше нет коммитов слияний.
Добавив интерактивный режим в ветку master, мы внесли изменения, конфликтующие с изменениями в ветке style. Вернёмся в ветке master в точку перед внесением конфликтующих изменений. Это позволяет нам продемонстрировать работу команды git rebase, не беспокоясь о конфликтах.
Коммит «Added README» идёт непосредственно перед коммитом конфликтующего интерактивного режима. Мы сбросим ветку master к коммиту «Added README».
Лог выглядит, как будто репозиторий был перемотан назад во времени к точке до какого-либо слияния.
Используем команду rebase вместо команды merge. Мы вернулись в точку до первого слияния и хотим перенести изменения из ветки master в нашу ветку style. На этот раз для переноса изменений из ветки master мы используем команду git rebase вместо слияния.
Конечный результат перебазирования очень похож на результат слияния. Ветка style в настоящее время содержит все свои изменения, а также все изменения ветки master. Однако, дерево коммитов значительно отличается. Дерево коммитов ветки style было переписано таким образом, что ветка master является частью истории коммитов. Это делает цепь коммитов линейной и гораздо более читабельной.
Мы поддерживали соответствие ветки style с веткой master (с помощью rebase), теперь же сольём изменения style в ветку master.
Выполним следующее.
Поскольку последний коммит ветки master прямо предшествует последнему коммиту ветки style, git может выполнить ускоренное слияние-перемотку. При быстрой перемотке вперёд git просто передвигает указатель вперёд, таким образом указывая на тот же коммит, что и ветка style.
При быстрой перемотке конфликтов быть не может.
Выполним следующее.
Теперь ветки style и master идентичны.
Перейдём в рабочий каталог и сделаем клон нашего репозитория hello.
Сейчас мы находимся в рабочем каталоге. Здесь есть единственный репозиторий под названием «hello».
Создадим клон репозитория.
В нашем рабочем каталоге теперь есть два репозитория: оригинальный репозиторий «hello» и клонированный репозиторий «cloned_hello».
Мы видим список всех файлов на верхнем уровне оригинального репозитория README.md, index.html и lib.
Выполним следующее.
Мы видим список всех коммитов в новый репозиторий, и он (более или менее) совпадает с историей коммитов в оригинальном репозитории. Единственная разница должна быть в названиях веток.
Мы видим ветку master (HEAD) в списке истории. Мы также видим ветки со странными именами (origin/master, origin/style и origin/HEAD).
Выполним следующее.
Мы видим, что клонированный репозиторий знает об имени по умолчанию удалённого репозитория. Посмотрим, можем ли мы получить более подробную информацию об имени по умолчанию.
Выполним следующее.
Давайте посмотрим на ветки, доступные в нашем клонированном репозитории.
Как мы видим, в списке только ветка master. Где ветка style? Команда git branch выводит только список локальных веток по умолчанию.
Для того, чтобы увидеть все ветки, попробуем следующую команду.
Git выводит все коммиты в оригинальный репозиторий, но ветки в удалённом репозитории не рассматриваются как локальные. Если мы хотим собственную ветку style, мы должны сами её создать. Скоро мы увидим, как это делается.
Внесём некоторые изменения в оригинальный репозиторий, чтобы затем попытаться извлечь и слить изменения из удалённой ветки в текущую.
Выполним следующее.
Примечание: Сейчас мы находимся в репозитории hello
Внесём следующие изменения в файл README.md.
Выполним следующее.
Теперь в оригинальном репозитории есть более поздние изменения, которых нет в клонированной версии. Далее мы извлечём и сольём эти изменения в клонированный репозиторий.
Научимся извлекать изменения из удалённого репозитория.
Сейчас мы находимся в репозитории cloned_hello.
На данный момент в репозитории есть все коммиты из оригинального репозитория, но они не интегрированы в локальные ветки клонированного репозитория.
В истории выше найдём коммит «Changed README in original repo». Обратим внимание, что коммит включает в себя коммиты «origin/master» и «origin/HEAD».
Теперь посмотрим на коммит «Updated index.html». Мы увидим, что локальная ветка master указывает на этот коммит, а не на новый коммит, который мы только что извлекли.
Выводом является то, что команда git fetch будет извлекать новые коммиты из удалённого репозитория, но не будет сливать их с нашими наработками в локальных ветках.
Мы можем продемонстрировать, что клонированный файл README.md не изменился.
Выполним следующее.
Сейчас мы увидим изменения.
Хотя команда git fetch не сливает изменения, мы можем вручную слить изменения из удалённого репозитория.
Теперь давайте рассмотрим объединение fetch и merge в одну команду.
Ветки, которые начинаются с remotes/origin являются ветками оригинального репозитория. Обратим внимание, что у нас больше нет ветки под названием style, но система контроля версий знает, что в оригинальном репозитории ветка style была.
Выполним следующее.
Теперь мы можем видеть ветку style в списке веток и логе.
Чистые репозитории (без рабочих каталогов) обычно используются для расшаривания. Обычный git-репозиторий подразумевает, что мы будем использовать его как рабочую директорию, поэтому вместе с файлами проекта в актуальной версии, git хранит все служебные, «чисто-репозиториевские» файлы в поддиректории .git. В удалённых репозиториях нет смысла хранить рабочие файлы на диске (как это делается в рабочих копиях), а все что им действительно нужно – это дельты изменений и другие бинарные данные репозитория. Вот это и есть «чистый репозиторий».
Сейчас мы находимся в рабочем каталоге.
Как правило, репозитории, оканчивающиеся на .git являются чистыми репозиториями. Мы видим, что в репозитории hello.git нет рабочего каталога. По сути, это есть не что иное, как каталог .git нечистого репозитория.
Давайте добавим репозиторий hello.git к нашему оригинальному репозиторию.
Так как чистые репозитории, как правило, расшариваются на каком-нибудь сетевом сервере, нам необходимо отправить наши изменения в другие репозитории. Начнём с создания изменения для отправки.
Отредактируем файл README.md и сделаем коммит.
Теперь отправим изменения в общий репозиторий.
Общим называется репозиторий, получающий отправленные нами изменения.
Научимся извлекать изменения из общего репозитория. Быстро переключимся в клонированный репозиторий и извлечём изменения, только что отправленные в общий репозиторий.
Получены навыки работы с системой git.